<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/>
<meta name="description" content="An example of virtualization where a virtualized TreeLayout sets the bounds for each node data."/> 
<link rel="stylesheet" href="../assets/css/style.css"/> 
<!-- Copyright 1998-2022 by Northwoods Software Corporation. -->
<title>Virtualized Tree with TreeLayout</title>
</head>

<body>
  <!-- This top nav is not part of the sample code -->
  <nav id="navTop" class="w-full z-30 top-0 text-white bg-nwoods-primary">
    <div class="w-full container max-w-screen-lg mx-auto flex flex-wrap sm:flex-nowrap items-center justify-between mt-0 py-2">
      <div class="md:pl-4">
        <a class="text-white hover:text-white no-underline hover:no-underline
        font-bold text-2xl lg:text-4xl rounded-lg hover:bg-nwoods-secondary " href="../">
          <h1 class="mb-0 p-1 ">GoJS</h1>
        </a>
      </div>
      <button id="topnavButton" class="rounded-lg sm:hidden focus:outline-none focus:ring" aria-label="Navigation">
        <svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6">
          <path id="topnavOpen" fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z" clip-rule="evenodd"></path>
          <path id="topnavClosed" class="hidden" fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
        </svg>
      </button>
      <div id="topnavList" class="hidden sm:block items-center w-auto mt-0 text-white p-0 z-20">
        <ul class="list-reset list-none font-semibold flex justify-end flex-wrap sm:flex-nowrap items-center px-0 pb-0">
          <li class="p-1 sm:p-0"><a class="topnav-link" href="../learn/">Learn</a></li>
          <li class="p-1 sm:p-0"><a class="topnav-link" href="../samples/">Samples</a></li>
          <li class="p-1 sm:p-0"><a class="topnav-link" href="../intro/">Intro</a></li>
          <li class="p-1 sm:p-0"><a class="topnav-link" href="../api/">API</a></li>
          <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/products/register.html">Register</a></li>
          <li class="p-1 sm:p-0"><a class="topnav-link" href="../download.html">Download</a></li>
          <li class="p-1 sm:p-0"><a class="topnav-link" href="https://forum.nwoods.com/c/gojs/11">Forum</a></li>
          <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/contact.html"
           target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/contact.html', 'contact');">Contact</a></li>
          <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/sales/index.html"
           target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/sales/index.html', 'buy');">Buy</a></li>
        </ul>
      </div>
    </div>
    <hr class="border-b border-gray-600 opacity-50 my-0 py-0" />
  </nav>
  <div class="md:flex flex-col md:flex-row md:min-h-screen w-full max-w-screen-xl mx-auto">
    <div id="navSide" class="flex flex-col w-full md:w-48 text-gray-700 bg-white flex-shrink-0"></div>
    <!-- * * * * * * * * * * * * * -->
    <!-- Start of GoJS sample code -->
    
    <script src="../release/go.js"></script>
    <div class="p-4 w-full">
<script id="code">
  // this controls whether the tree grows deeper towards the right or downwards:
  const HORIZONTAL = true;

  function init() {
    if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this

    // Since 2.2 you can also author concise templates with method chaining instead of GraphObject.make
    // For details, see https://gojs.net/latest/intro/buildingObjects.html
    const $ = go.GraphObject.make;  // for conciseness in defining templates

    // The Diagram just shows what should be visible in the viewport.
    // Its model does NOT include node data for the whole graph, but only that
    // which might be visible in the viewport.
    myDiagram =
      $(go.Diagram, "myDiagramDiv",
        {
          initialDocumentSpot: go.Spot.Center,
          initialViewportSpot: go.Spot.Center,

          // Use a virtualized TreeLayout which does not require
          // that the Nodes and Links exist first for an accurate layout
          layout:
            $(VirtualizedTreeLayout,
              { angle: (HORIZONTAL ? 0 : 90), nodeSpacing: 10 }),

          // Define the template for Nodes, used by virtualization.
          nodeTemplate:
            $(go.Node, "Auto",
              { isLayoutPositioned: false },  // optimization
              new go.Binding("position", "bounds", b => b.position)
                .makeTwoWay((p, d) => new go.Rect(p.x, p.y, d.bounds.width, d.bounds.height)),
              { width: 70, height: 20 },  // in cooperation with the load function, below
              $(go.Shape, "Rectangle",
                new go.Binding("fill", "color")),
              $(go.TextBlock,
                { margin: 2 },
                new go.Binding("text", "color")),
              {
                toolTip:
                  $("ToolTip",
                    $(go.TextBlock, { margin: 3 },
                      new go.Binding("text", "", d => "key: " + d.key + "\nbounds: " + d.bounds.toString()))
                  )
              }
            ),

          // Define the template for Links
          linkTemplate:
            $(go.Link,
              {
                fromSpot: (HORIZONTAL ? go.Spot.Right : go.Spot.Bottom),
                toSpot: (HORIZONTAL ? go.Spot.Left : go.Spot.Top)
              },
              $(go.Shape)
            ),

          "SelectionMoved": e => {
            e.subject.each(n => {
              if (n instanceof go.Node) n.data.points = undefined;
            })
          },

          "animationManager.isEnabled": false
        });

    // This model includes all of the data
    myWholeModel =
      new go.TreeModel();  // must match the model used by the Diagram, below

    // The virtualized layout works on the full model, not on the Diagram Nodes and Links
    myDiagram.layout.model = myWholeModel;

    // Do not set myDiagram.model = myWholeModel -- that would create a zillion Nodes and Links!
    // In the future Diagram may have built-in support for virtualization.
    // For now, we have to implement virtualization ourselves by having the Diagram's model
    // be different than the "real" model.
    myDiagram.model =   // this only holds nodes that should be in the viewport
      new go.TreeModel();  // must match the model, above

    // for now, we have to implement virtualization ourselves
    myDiagram.isVirtualized = true;
    myDiagram.addDiagramListener("ViewportBoundsChanged", onViewportChanged);

    myDiagram.delayInitialization(diagram => spinDuring(diagram, "mySpinner", load));
  }

  // implement a wait spinner in HTML with CSS animation
  function spinDuring(diagram, spinner, compute) {  // where compute is a function of zero args
    // show the animated spinner
    if (typeof spinner === "string") spinner = document.getElementById(spinner);
    if (spinner) {
      // position it in the middle of the viewport DIV
      const x = Math.floor(diagram.div.offsetWidth/2 - spinner.naturalWidth/2);
      const y = Math.floor(diagram.div.offsetHeight/2 - spinner.naturalHeight/2);
      spinner.style.left = x + "px";
      spinner.style.top = y + "px";
      spinner.style.display = "inline";
    }
    setTimeout(() => {
      try {
        compute();  // do the computation
      } finally {
        if (spinner) spinner.style.display = "none";
      }
    }, 20);
  }

  function load() {
    // create a lot of data for the myWholeModel
    const total = 123456;
    const treedata = [];
    for (let i = 0; i < total; i++) {
      const d = {
        key: i,  // this node data's key
        color: go.Brush.randomColor(),  // the node's color
        parent: (i > 0 ? Math.floor(Math.random() * i / 2) : undefined)  // the random parent's key
      };
      //!!!???@@@ this needs to be customized to account for your chosen Node template
      d.bounds = new go.Rect(0, 0, 70, 20);
      treedata.push(d);
    }
    myWholeModel.nodeDataArray = treedata;
    myDiagram.layoutDiagram(true);
  }


  // The following functions implement virtualization of the Diagram
  // Assume data.bounds is a Rect of the area occupied by the Node in document coordinates.

  // The normal mechanism for determining the size of the document depends on all of the
  // Nodes and Links existing, so we need to use a function that depends only on the model data.
  function computeDocumentBounds() {
    const b = new go.Rect();
    const ndata = myWholeModel.nodeDataArray;
    for (let i = 0; i < ndata.length; i++) {
      const d = ndata[i];
      if (!d.bounds) continue;
      if (i === 0) {
        b.set(d.bounds);
      } else {
        b.unionRect(d.bounds);
      }
    }
    return b;
  }

  // As the user scrolls or zooms, make sure the Parts (Nodes and Links) exist in the viewport.
  function onViewportChanged(e) {
    const diagram = e.diagram;
    // make sure there are Nodes for each node data that is in the viewport
    // or that is connected to such a Node
    const viewb = diagram.viewportBounds;  // the new viewportBounds
    const model = diagram.model;

    const oldskips = diagram.skipsUndoManager;
    diagram.skipsUndoManager = true;

    const b = new go.Rect();
    const ndata = myWholeModel.nodeDataArray;
    for (let i = 0; i < ndata.length; i++) {
      const n = ndata[i];
      if (model.containsNodeData(n)) continue;
      if (!n.bounds) continue;
      if (n.bounds.intersectsRect(viewb)) {
        model.addNodeData(n);
      }
      if (model instanceof go.TreeModel) {
        // make sure links to all parent nodes appear
        const parentkey = myWholeModel.getParentKeyForNodeData(n);
        const parent = myWholeModel.findNodeDataForKey(parentkey);
        if (parent !== null) {
          if (n.bounds.intersectsRect(viewb)) {  // N is inside viewport
            model.addNodeData(parent);  // so that link to parent appears
            const child = diagram.findNodeForData(n);
            if (child !== null) {
              const link = child.findTreeParentLink();
              if (link !== null) {
                // do this now to avoid delayed routing outside of transaction
                link.fromNode.ensureBounds();
                link.toNode.ensureBounds();
                if (child.data.fromSpot) link.fromSpot = child.data.fromSpot;
                if (child.data.toSpot) link.toSpot = child.data.toSpot;
                if (child.data.points) {
                  link.points = child.data.points;
                } else {
                  link.updateRoute();
                }
              }
            }
          } else {  // N is outside of viewport
            // see if there's a parent that is in the viewport,
            // or if the link might cross over the viewport
            b.set(n.bounds);
            b.unionRect(parent.bounds);
            if (b.intersectsRect(viewb)) {
              model.addNodeData(n);  // add N so that link to parent appears
              const child = diagram.findNodeForData(n);
              if (child !== null) {
                const link = child.findTreeParentLink();
                if (link !== null) {
                  // do this now to avoid delayed routing outside of transaction
                  link.fromNode.ensureBounds();
                  link.toNode.ensureBounds();
                  if (child.data.fromSpot) link.fromSpot = child.data.fromSpot;
                  if (child.data.toSpot) link.toSpot = child.data.toSpot;
                  if (child.data.points) {
                    link.points = child.data.points;
                  } else {
                    link.updateRoute();
                  }
                }
              }
            }
          }
        }
      }
    }

    if (model instanceof go.GraphLinksModel) {
      const ldata = myWholeModel.linkDataArray;
      for (let i = 0; i < ldata.length; i++) {
        const l = ldata[i];
        const fromkey = myWholeModel.getFromKeyForLinkData(l);
        if (fromkey === undefined) continue;
        const from = myWholeModel.findNodeDataForKey(fromkey);
        if (from === null || !from.bounds) continue;

        const tokey = myWholeModel.getToKeyForLinkData(l);
        if (tokey === undefined) continue;
        const to = myWholeModel.findNodeDataForKey(tokey);
        if (to === null || !to.bounds) continue;

        b.set(from.bounds);
        b.unionRect(to.bounds);
        if (b.intersectsRect(viewb)) {
          // also make sure both connected nodes are present,
          // so that link routing is authentic
          model.addNodeData(from);
          model.addNodeData(to);
          model.addLinkData(l);
          const link = diagram.findLinkForData(l);
          if (link !== null) {
            // do this now to avoid delayed routing outside of transaction
            link.fromNode.ensureBounds();
            link.toNode.ensureBounds();
            if (link.data.fromSpot) link.fromSpot = link.data.fromSpot;
            if (link.data.toSpot) link.toSpot = link.data.toSpot;
            //if (link.data.points) {
            //  link.points = link.data.points;
            //} else {
              link.updateRoute();
            //}
          }
        }
      }
    }

    diagram.skipsUndoManager = oldskips;

    if (myRemoveTimer === null) {
      // only remove offscreen nodes after a delay
      myRemoveTimer = setTimeout(() => removeOffscreen(diagram), 3000);
    }

    updateCounts();  // only for this sample
  }

  // occasionally remove Parts that are offscreen from the Diagram
  var myRemoveTimer = null;

  function removeOffscreen(diagram) {
    myRemoveTimer = null;

    const viewb = diagram.viewportBounds;
    const model = diagram.model;
    const remove = [];  // collect for later removal
    const removeLinks = new go.Set();  // links connected to a node data to remove
    const it = diagram.nodes;
    while (it.next()) {
      const n = it.value;
      const d = n.data;
      if (d === null) continue;
      if (!n.actualBounds.intersectsRect(viewb) && !n.isSelected) {
        // even if the node is out of the viewport, keep it if it is selected or
        // if any link connecting with the node is still in the viewport
        if (!n.linksConnected.any(l => l.actualBounds.intersectsRect(viewb))) {
          remove.push(d);
          if (model instanceof go.GraphLinksModel) {
            removeLinks.addAll(n.linksConnected);
          }
        }
      }
    }

    if (remove.length > 0) {
      const oldskips = diagram.skipsUndoManager;
      diagram.skipsUndoManager = true;
      model.removeNodeDataCollection(remove);
      if (model instanceof go.GraphLinksModel) {
        removeLinks.each(l => {
          if (!l.isSelected) model.removeLinkData(l.data);
        });
      }
      diagram.skipsUndoManager = oldskips;
    }

    updateCounts();  // only for this sample
  }
  // end of virtualized Diagram


// start of VirtualizedTree[Layout/Network] classes

// Here we try to replace the dependence of TreeLayout on Nodes
// with depending only on the data in the TreeModel.
class VirtualizedTreeLayout extends go.TreeLayout {
  constructor() {
    super();
    this.isOngoing = false;
    this.model = null;  // add this property for holding the whole TreeModel
  }

  static _cachedLinks = [];

  createNetwork() {
    return new VirtualizedTreeNetwork(this);  // defined below
  }

  // ignore the argument, an (implicit) collection of Parts
  makeNetwork(coll) {
    const net = this.createNetwork();
    net.addData(this.model);  // use the model data, not any actual Nodes and Links
    return net;
  }

  commitLayout() {
    VirtualizedTreeEdge._dummyLink = this.diagram.linkTemplate.copy();
    VirtualizedTreeEdge._dummyFromNode = this.diagram.nodeTemplate.copy();
    VirtualizedTreeEdge._dummyToNode = this.diagram.nodeTemplate.copy();
    VirtualizedTreeEdge._dummyLink.fromNode = VirtualizedTreeEdge._dummyFromNode;
    VirtualizedTreeEdge._dummyLink.toNode = VirtualizedTreeEdge._dummyToNode;
    this.diagram.add(VirtualizedTreeEdge._dummyFromNode);
    this.diagram.add(VirtualizedTreeEdge._dummyToNode);
    this.diagram.add(VirtualizedTreeEdge._dummyLink);

    super.commitLayout();
    // can't depend on regular bounds computation that depends on all Nodes existing
    this.diagram.fixedBounds = computeDocumentBounds();

    this.diagram.remove(VirtualizedTreeEdge._dummyFromNode);
    this.diagram.remove(VirtualizedTreeEdge._dummyToNode);
    this.diagram.remove(VirtualizedTreeEdge._dummyLink);

    // update the positions of any existing Nodes
    this.diagram.nodes.each(node => node.updateTargetBindings());
  }

  setPortSpots(v) {
    v.destinationEdges.each(e => {
      e.link = VirtualizedTreeLayout._cachedLinks.pop() || new go.Link();
    });
    super.setPortSpots(v);
    v.destinationEdges.each(e => {
      if (e.data) {
        e.data.fromSpot = e.link.fromSpot.copy();
        e.data.toSpot = e.link.toSpot.copy();
      }
      VirtualizedTreeLayout._cachedLinks.push(e.link);
      e.link = null;
    });
  }
}
// end VirtualizedTreeLayout class


class VirtualizedTreeNetwork extends go.TreeNetwork {
  constructor(layout) {
    super(layout);
  }

  createEdge() {
    return new VirtualizedTreeEdge(this);
  }

  addData(model) {
    if (model instanceof go.TreeModel) {
      const dataVertexMap = new go.Map();
      const ndata = model.nodeDataArray;
      for (let i = 0; i < ndata.length; i++) {
        const d = ndata[i];
        const v = this.createVertex();
        v.data = d;  // associate this Vertex with data, not a Node
        dataVertexMap.set(d, v);
        this.addVertex(v);
      }

      for (let i = 0; i < ndata.length; i++) {
        const child = ndata[i];
        const parentkey = model.getParentKeyForNodeData(child);
        const parent = model.findNodeDataForKey(parentkey);
        if (parent !== null) {  // if there is a parent, there should be an edge
          // now find corresponding vertexes
          const f = dataVertexMap.get(parent);
          const t = dataVertexMap.get(child);
          if (f === null || t === null) continue;  // skip
          // create and add VirtualizedTreeEdge
          const e = this.createEdge();
          e.data = child;  // associate this Edge with data, not a Link
          e.fromVertex = f;
          e.toVertex = t;
          this.addEdge(e);
        }
      }
    } else {
      throw new Error("can only handle TreeModel data");
    }
  }
}
// end VirtualizedTreeNetwork class

class VirtualizedTreeEdge extends go.TreeEdge {
  constructor(network) {
    super(network);
  }

  static _dummyLink = null;
  static _dummyFromNode = null;
  static _dummyToNode = null;

  commit() {
    const parentv = this.fromVertex;
    if (!parentv) return;
    const routed = (parentv.alignment === go.TreeLayout.AlignmentStart || parentv.alignment === go.TreeLayout.AlignmentEnd);
    if (this.data && routed) {
      this.link = VirtualizedTreeEdge._dummyLink;
      this.link.fromNode.position = new go.Point(this.fromVertex.x, this.fromVertex.y);
      this.link.toNode.position = new go.Point(this.toVertex.x, this.toVertex.y);
      this.link.fromNode.ensureBounds();
      this.link.toNode.ensureBounds();
      this.link.updateRoute();
    }
    super.commit();
    if (this.data && routed) {
      this.data.points = this.link.points.copy();
      this.link = null;
    }
  }
}

// end of VirtualizedTree[Layout/Network] classes

  // This function is only used in this sample to demonstrate the effects of the virtualization.
  // In a real application you would delete this function and all calls to it.
  function updateCounts() {
    document.getElementById("myMessage1").textContent = myWholeModel.nodeDataArray.length;
    document.getElementById("myMessage2").textContent = myDiagram.nodes.count;
    document.getElementById("myMessage4").textContent = myDiagram.links.count;
  }

  window.addEventListener('DOMContentLoaded', init);
</script>
<style>
  #mySpinner { display: none; position: absolute; z-index: 100; animation: spin 1s linear infinite; }
  @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
</style>

<div id="sample">
  <div id="myDiagramDiv" style="background-color: white; border: solid 1px black; width: 100%; height: 600px"></div>
  <img id="mySpinner" src="images/spinner.png">
  <div id="description">
  <p>This uses a <a>TreeModel</a> and a virtualized <a>TreeLayout</a>.</p>
  Node data in Model: <span id="myMessage1"></span>.
  Actual Nodes in Diagram: <span id="myMessage2"></span>.
  Actual Links in Diagram: <span id="myMessage4"></span>.
  </div>
</div>
    </div>
    <!-- * * * * * * * * * * * * * -->
    <!--  End of GoJS sample code  -->
  </div>
</body>
<!--  This script is part of the gojs.net website, and is not needed to run the sample -->
<script src="../assets/js/goSamples.js"></script>
</html>
